package org.swift.mashup.engine.model;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

import net.sf.saxon.Configuration;
import net.sf.saxon.om.DocumentInfo;
import net.sf.saxon.om.NamespaceResolver;
import net.sf.saxon.query.DynamicQueryContext;
import net.sf.saxon.query.StaticQueryContext;
import net.sf.saxon.query.XQueryExpression;
import net.sf.saxon.trans.XPathException;

import org.apache.log4j.Logger;
import org.apache.xerces.dom.DeferredTextImpl;
import org.swift.commons.xml.XmlUtils;
import org.swift.mashup.engine.concurrency.memory.MemoryItem;
import org.w3c.dom.Node;

@SuppressWarnings("unchecked")
public class XQueryParameterProcessor implements ParameterProcessor {
	private static final Logger log = Logger
		.getLogger(XQueryParameterProcessor.class);

	private Collection<String> withNames;
	private Map<String, ExpressionContext> parameterDefinitions;
	private NamespaceResolver uriResolver;

	public XQueryParameterProcessor(Collection<String> withNames, NamespaceResolver resolver) {
		this.withNames = Collections.unmodifiableCollection(withNames);
		this.parameterDefinitions = new HashMap<String, ExpressionContext>();
		this.uriResolver = resolver;
	}

	public Map<String,? extends Object> extractParameters(
			Map<String, ?> memoryItems) {
		Map<String, Object> parameters = new HashMap<String, Object>();
		for (Entry<String, ExpressionContext> entry : parameterDefinitions
			.entrySet()) {
			parameters.put(entry.getKey(), entry.getValue().evaluateFor(memoryItems));
		}
		return parameters;
	}

	/**
	 * Provides a definition of a parameter whose value is to be derrived by executing
	 * an XPAth expression in XQuery.
	 * 
	 * @param parameterName the parameter that will be defined with a value set to the
	 * result of the expression
	 * @param xqueryExpression the xpath to run to get the value for the parameter
	 */
	public void addParameterSelector(String parameterName, String xqueryExpression) {
		parameterDefinitions.put(parameterName, buildExpressionContext(xqueryExpression));
	}
	
	/**
	 * This will actually avoid the XQuery engine and just yield a parameter named
	 * <code>parameterName</code> that has a matching value of the with whose name is <code>withName</code>
	 * 
	 * @param parameterName 
	 * @param withName
	 */
	public void addParameterFromWith (String parameterName, String withName){
		parameterDefinitions.put(parameterName, new ExpressionContext(withName));
	}

	protected ExpressionContext buildExpressionContext(String xpath) {
		// create a Configuration object
		Configuration C = new Configuration();
		// static and dynamic context
		StaticQueryContext SQC = new StaticQueryContext(C);
		SQC.setExternalNamespaceResolver(uriResolver);
		try {
			String newExpression = "\n";
			for (String parameter : withNames) {
				newExpression = newExpression + "declare variable $"
					+ parameter + " external;\n";
			}
			if (xpath.indexOf("(") == -1){
				xpath = "string(" + (xpath.startsWith("$") ? xpath : "'" + xpath + "'") + ")";
			}
			newExpression = newExpression + "for $doc in . return " + xpath;
			log.debug(newExpression);
			return new ExpressionContext(SQC.compileQuery(newExpression));
		} catch (net.sf.saxon.trans.XPathException e) {
			throw new RuntimeException(e);
		}
	}

	protected class ExpressionContext {
		private XQueryExpression expression;
		private String mirror = null;
		private DynamicQueryContext queryContext;

		public ExpressionContext(XQueryExpression xqe) {
			this.expression = xqe;
		}
		
		public ExpressionContext(String mirror){
			this.mirror = mirror;
		}

		public synchronized Object evaluateFor(
				Map<String, ?> memoryItems) {
			if (isMirror()){
				return findMirror (memoryItems);
			}
			try {
				if (this.queryContext == null) {
					buildQueryContext();
				}
				setContextParameters(memoryItems);
				Object results = expression.evaluate(queryContext); 
				if (results != null){
					return handleResultSet ((ArrayList)results);
				}
				return null;
			} catch (XPathException xpe) {
				throw new RuntimeException(xpe);
			}
		}
		
		private Object handleResultSet (ArrayList results){
			ArrayList resultList = new ArrayList();
			for (Object resultItem : (ArrayList) results){
				if (resultItem instanceof DeferredTextImpl){
					resultItem = ((DeferredTextImpl) resultItem).getTextContent();
				}
				else if (resultItem instanceof Node){
					resultItem = XmlUtils.toString((Node)resultItem);
				}
				resultList.add(resultItem);
			}
			if (resultList.size() == 1){
				return resultList.get(0);
			} else {
				return resultList.toArray(new Object[0]);
			}
		}
		
		private boolean isMirror (){
			return this.mirror != null;
		}
		
		private Object findMirror (Map<String, ?> memoryItems){
			Object result = memoryItems.get(this.mirror);
			if (result instanceof MemoryItem){
				return result.toString();		
			} else {
				return result;
			}
		}

		private void setContextParameters(Map<String, ? extends Object> memoryItems) {
			queryContext.clearParameters();
			Node node = null;
			for (Entry<String, ? extends Object> entry : memoryItems.entrySet()) {
				Object item = entry.getValue();
				String strValue = item.toString();
				if (XmlUtils.isXml(strValue)) {
					if (item instanceof MemoryItem){
						node = ((MemoryItem)item).toNode();
					} else {
						node = XmlUtils.toNode(strValue);
					}
					log.debug("Setting context parameter " + entry.getKey() + " to node " + XmlUtils.toString(node));
					queryContext.setParameter(entry.getKey(), node);
				} else {
					log.debug("Setting context parameter " + entry.getKey() + " to value " + strValue);
					queryContext.setParameter(entry.getKey(), strValue);
				}
			}
		}

		private void buildQueryContext() throws XPathException {
			DocumentInfo documentInfo = null;
			StaticQueryContext staticQueryContext = expression
				.getStaticContext().getUserQueryContext();
			staticQueryContext.setExternalNamespaceResolver(uriResolver);
			Source source = new StreamSource(new StringReader(
				"<condition></condition>"));
			documentInfo = staticQueryContext.buildDocument(source);
			queryContext = new DynamicQueryContext(staticQueryContext
				.getConfiguration());
			queryContext.setContextItem(documentInfo);
		}
	}

}
